package org.salatec.tapestry4e.builder;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceDelta;
import org.eclipse.core.resources.IResourceDeltaVisitor;
import org.eclipse.core.resources.IResourceVisitor;
import org.eclipse.core.resources.IncrementalProjectBuilder;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Path;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.salatec.tapestry4e.text.TMLContentAssistProcessor;
import org.salatec.tapestry4e.util.Logger;
import org.salatec.tapestry4e.util.T5ComponentsStore;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

public class TapestryBuilder extends IncrementalProjectBuilder {

	class TapestryDeltaVisitor implements IResourceDeltaVisitor {
		/*
		 * (non-Javadoc)
		 * 
		 * @see
		 * org.eclipse.core.resources.IResourceDeltaVisitor#visit(org.eclipse
		 * .core.resources.IResourceDelta)
		 */
		public boolean visit(IResourceDelta delta) throws CoreException {
			int kind = delta.getKind();
			IResource resource = delta.getResource();
			if (resource instanceof IFile
					&& "java".equals(resource.getFileExtension()))
				if (kind == IResourceDelta.ADDED
						|| kind == IResourceDelta.CHANGED)
					addResources.add(resource);
				else if (kind == IResourceDelta.REMOVED)
					removeResources.add(resource);

			return performWebXMLBuild(resource);
		}
	}

	class TapestryResourceVisitor implements IResourceVisitor {
		@SuppressWarnings("restriction")
		public boolean visit(IResource resource) {

			if (resource instanceof IFile
					&& "java".equals(resource.getFileExtension()))
				addResources.add(resource);
			return performWebXMLBuild(resource);
		}
	}

	public static final String BUILDER_ID = "org.salatec.tapestry4e.tapestryBuilder";

	private static final String MARKER_TYPE = "org.salatec.tapestry4e.xmlProblem";

	private static final String TAPESTRY_FILTER_CLASS = "org.apache.tapestry5.TapestryFilter";

	private T5ComponentsStore store;

	private DocumentBuilderFactory parserFactory;

	private List<IResource> addResources;

	private List<IResource> removeResources;

	private List<String> appPackages;

	private List<String> sourceFolders;

	private IJavaProject javaProject;

	private void addMarker(IFile file, String message, int lineNumber,
			int severity) {
		try {
			IMarker marker = file.createMarker(MARKER_TYPE);
			marker.setAttribute(IMarker.MESSAGE, message);
			marker.setAttribute(IMarker.SEVERITY, severity);
			if (lineNumber == -1) {
				lineNumber = 1;
			}
			marker.setAttribute(IMarker.LINE_NUMBER, lineNumber);
		} catch (CoreException e) {
		}
	}

	private boolean performWebXMLBuild(IResource resource) {
		String name = resource.getName();
		if (resource instanceof IFile && "web.xml".equalsIgnoreCase(name)) {
			IFile file = (IFile) resource;
			deleteMarkers(file);

			Document doc;
			try {
				doc = getParser().parse(file.getContents());
				if (!checkTapestryFilterExists(doc.getDocumentElement())) {
					addMarker(
							file,
							"This Web application is not configured for Tapestry. Missing Tapestry Filter configuration in web.xml.",
							0, IMarker.SEVERITY_ERROR);
					// Stop Building NOW!
					return false;
				} else {
					// Find Application package from web.xml and store it in
					// the project properties.
					String webxmlpackage = getT5ApplicationPackageFromWebXML(doc
							.getDocumentElement());
					if (webxmlpackage != null)
						store.addApplicationPackage(webxmlpackage);
					appPackages = store.getApplicationPackages();

				}

			} catch (Exception e) {
				addMarker(file, e.getMessage(), 0, IMarker.SEVERITY_ERROR);
				return false;
			}

		}
		// return true to continue visiting children.
		return true;
	}

	boolean checkTapestryFilterExists(Element documentElement) {
		NodeList children = documentElement.getElementsByTagName("filter");
		for (int i = 0; i < children.getLength(); i++) {
			Element filter = (Element) children.item(i);
			String filterClass = filter.getElementsByTagName("filter-class")
					.item(0).getFirstChild().getNodeValue();
			if (TAPESTRY_FILTER_CLASS.equalsIgnoreCase(filterClass))
				return true;

		}

		return false;
	}

	private String getT5ApplicationPackageFromWebXML(Element documentElement) {
		NodeList children = documentElement
				.getElementsByTagName("context-param");
		for (int i = 0; i < children.getLength(); i++) {
			Element contextParam = (Element) children.item(i);
			String paramName = contextParam.getElementsByTagName("param-name")
					.item(0).getFirstChild().getNodeValue();
			if ("tapestry.app-package".equalsIgnoreCase(paramName)) {
				Element paramValue = (Element) contextParam
						.getElementsByTagName("param-value").item(0);
				return paramValue.getFirstChild().getNodeValue();
			}
		}

		return null;
	}

	private void updateComponentsStore() throws JavaModelException {
		javaProject = JavaCore.create(getProject());

		sourceFolders = new ArrayList<String>();
		for (IClasspathEntry entry : javaProject.getRawClasspath()) {
			if (entry.getEntryKind() == IClasspathEntry.CPE_SOURCE) {
				// This is a source folder
				String path = entry.getPath().toString().substring(1);
				path = path.substring(path.indexOf(File.separator) + 1);
				sourceFolders.add(path);
			}
		}
		if (addResources != null) {
			for (IResource resource : addResources) {
				updateStoreForResource(resource, true);
			}

		}
		if (removeResources != null) {
			for (IResource resource : removeResources) {
				updateStoreForResource(resource, false);
			}
		}
		try {
			TMLContentAssistProcessor.setProjectComponents(store
					.getComponents());
		} catch (Exception e) {
			Logger.log(Logger.WARNING, "Error setting project components", e);
		}
	}

	private void updateStoreForResource(IResource resource, boolean add)
			throws JavaModelException {
		IFile file = (IFile) resource;
		IPath path = file.getProjectRelativePath();
		String className = findClassName(path.toString());
		String packageName = "";
		if (className.indexOf('.') > 0)
			packageName = className.substring(0, className.lastIndexOf('.'));
		path = findPathRelativeToClasspath(path.toString());
		ICompilationUnit unit = (ICompilationUnit) javaProject
				.findElement(path);
		for (String appPackage : appPackages) {
			if (packageName.equalsIgnoreCase(appPackage + ".components")) {
				if (add)
					store.add(unit);
				else
					store.remove(unit);
			}
		}
	}

	private IPath findPathRelativeToClasspath(String pathString) {
		for (String sourceFolder : sourceFolders) {
			if (pathString.indexOf(sourceFolder) >= 0) {
				pathString = pathString.substring(sourceFolder.length() + 1);
				return new Path(pathString);
			}
		}
		return null;
	}

	private String findClassName(String pathString) {
		for (String sourceFolder : sourceFolders) {
			if (pathString.indexOf(sourceFolder) >= 0) {
				pathString = pathString.substring(sourceFolder.length() + 1);
			}
		}
		pathString = pathString.replaceAll(File.separator, ".");
		pathString = pathString.replace(".java", "");
		return pathString;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.core.internal.events.InternalBuilder#build(int,
	 * java.util.Map, org.eclipse.core.runtime.IProgressMonitor)
	 */
	protected IProject[] build(int kind, Map args, IProgressMonitor monitor)
			throws CoreException {
		try {
			store = new T5ComponentsStore(getProject());
			appPackages = store.getApplicationPackages();
			addResources = new ArrayList<IResource>();
			removeResources = new ArrayList<IResource>();
		} catch (Exception e) {
			Logger.log(Logger.ERROR,
					"Unable to create component store, exception: "
							+ e.getMessage());
			return null;
		}
		if (kind == FULL_BUILD) {
			fullBuild(monitor);
		} else {
			IResourceDelta delta = getDelta(getProject());
			if (delta == null) {
				fullBuild(monitor);
			} else {
				incrementalBuild(delta, monitor);
			}
		}

		updateComponentsStore();

		return null;
	}

	private void deleteMarkers(IFile file) {
		try {
			file.deleteMarkers(MARKER_TYPE, false, IResource.DEPTH_ZERO);
		} catch (CoreException ce) {
		}
	}

	protected void fullBuild(final IProgressMonitor monitor)
			throws CoreException {
		try {

			getProject().accept(new TapestryResourceVisitor());
		} catch (CoreException e) {
		}
	}

	private DocumentBuilder getParser() throws ParserConfigurationException,
			SAXException {
		if (parserFactory == null) {
			parserFactory = DocumentBuilderFactory.newInstance();
		}
		return parserFactory.newDocumentBuilder();
	}

	protected void incrementalBuild(IResourceDelta delta,
			IProgressMonitor monitor) throws CoreException {
		// the visitor does the work.
		delta.accept(new TapestryDeltaVisitor());
	}

	private boolean checkCancel(IProgressMonitor monitor) {

		if (monitor.isCanceled())
			throw new OperationCanceledException();
		return isInterrupted();
	}
}
